﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Windows.Forms;

namespace pk3DS.Core.CTR;

public class RomFS
{
    // Get Info
    public readonly string FileName;
    public readonly bool isTempFile;
    public readonly byte[] SuperBlockHash;
    public readonly uint SuperBlockLen;

    public RomFS(string fn)
    {
        FileName = fn;
        isTempFile = true;
        using var fs = File.OpenRead(fn);
        fs.Seek(0x8, SeekOrigin.Begin);
        uint mhlen = (uint)(fs.ReadByte() | (fs.ReadByte() << 8) | (fs.ReadByte() << 16) | (fs.ReadByte() << 24));
        SuperBlockLen = mhlen + 0x50;
        if (SuperBlockLen % 0x200 != 0)
            SuperBlockLen += 0x200 - (SuperBlockLen % 0x200);
        byte[] superblock = new byte[SuperBlockLen];
        fs.Seek(0, SeekOrigin.Begin);
        fs.Read(superblock, 0, superblock.Length);
        SuperBlockHash = SHA256.HashData(superblock);
    }

    // Build
    internal const int PADDING_ALIGN = 16;
    internal static string ROOT_DIR;
    internal const string TempFile = "tempRomFS.bin";
    internal static string OutFile;
    internal const uint ROMFS_UNUSED_ENTRY = 0xFFFFFFFF;

    internal static void UpdateTB(RichTextBox RTB, string progress)
    {
        try
        {
            if (RTB.InvokeRequired)
            {
                RTB.Invoke((MethodInvoker)delegate
                {
                    RTB.AppendText(Environment.NewLine + progress);
                    RTB.SelectionStart = RTB.Text.Length;
                    RTB.ScrollToCaret();
                });
            }
            else
            {
                RTB.SelectionStart = RTB.Text.Length;
                RTB.ScrollToCaret();
                RTB.AppendText(progress + Environment.NewLine);
            }
        }
        catch { }
    }

    public void ExtractRomFS(string outputDirectory, RichTextBox TB_Progress = null, ProgressBar PB_Show = null)
    {
        byte[] ivfcHeaderBytes = new byte[0x5C];
        using (var file = new FileStream(FileName, FileMode.Open, FileAccess.Read))
        {
            _ = file.Read(ivfcHeaderBytes, 0, ivfcHeaderBytes.Length);
        }
        var ivfc = GetIVFCInfoFromBytes(ivfcHeaderBytes);

        ulong romfsDataOffset = Align(ivfc.Levels[0].HashOffset + ivfc.MasterHashSize, ivfc.Levels[2].BlockSize);
        byte[] romfsInfoHeaderBytes = new byte[0x28];
        using (var file = new FileStream(FileName, FileMode.Open, FileAccess.Read))
        {
            file.Seek(Convert.ToInt64(romfsDataOffset), SeekOrigin.Begin);
            _ = file.Read(romfsInfoHeaderBytes, 0, romfsInfoHeaderBytes.Length);
        }
        Romfs_InfoHeader infoHeader = GetRomfsInfoHeaderFromBytes(romfsInfoHeaderBytes, Convert.ToUInt32(romfsDataOffset));

        byte[] directoryMetadataBlock = new byte[infoHeader.Sections[1].Size];
        byte[] fileMetadataBlock = new byte[infoHeader.Sections[3].Size];
        using (var file = new FileStream(FileName, FileMode.Open, FileAccess.Read))
        {
            file.Seek(Convert.ToInt64(infoHeader.Sections[1].Offset), SeekOrigin.Begin);
            _ = file.Read(directoryMetadataBlock, 0, directoryMetadataBlock.Length);

            file.Seek(Convert.ToInt64(infoHeader.Sections[3].Offset), SeekOrigin.Begin);
            _ = file.Read(fileMetadataBlock, 0, fileMetadataBlock.Length);
        }

        if (!Directory.Exists(outputDirectory))
        {
            Directory.CreateDirectory(outputDirectory);
        }

        VisitDirectory(0, outputDirectory, directoryMetadataBlock, fileMetadataBlock, infoHeader.DataOffset, TB_Progress, PB_Show);
    }

    private static IVFCInfo GetIVFCInfoFromBytes(byte[] headerBytes)
    {
        var ivfc = new IVFCInfo();
        uint magic1 = BitConverter.ToUInt32(headerBytes, 0x00);
        uint magic2 = BitConverter.ToUInt32(headerBytes, 0x04);
        if (magic1 != 0x43465649 || magic2 != 0x10000)
        {
            throw new ArgumentException("Bad IVFC Header");
        }
        ivfc.MasterHashSize = BitConverter.ToUInt32(headerBytes, 0x08);
        ivfc.Levels = new IVFCLevel[3];
        for (int i = 0; i < 3; i++)
        {
            ivfc.Levels[i] = new IVFCLevel
            {
                HashOffset = BitConverter.ToUInt64(headerBytes, 0x0C + (i * 0x18)),
                DataLength = BitConverter.ToUInt64(headerBytes, 0x14 + (i * 0x18)),
                BlockSize = 1U << BitConverter.ToInt32(headerBytes, 0x1C + (i * 0x18)),
            };
        }
        return ivfc;
    }

    private static Romfs_InfoHeader GetRomfsInfoHeaderFromBytes(byte[] headerBytes, uint bodyOffset)
    {
        var infoHeader = new Romfs_InfoHeader
        {
            HeaderLength = BitConverter.ToUInt32(headerBytes, 0x00),
            Sections = new Romfs_SectionHeader[4],
        };
        for (int i = 0; i < 4; i++)
        {
            infoHeader.Sections[i] = new Romfs_SectionHeader
            {
                Offset = BitConverter.ToUInt32(headerBytes, 0x04 + (i * 0x08)) + bodyOffset,
                Size = BitConverter.ToUInt32(headerBytes, 0x08 + (i * 0x08)),
            };
        }
        infoHeader.DataOffset = BitConverter.ToUInt32(headerBytes, 0x24) + bodyOffset;
        return infoHeader;
    }

    private void VisitDirectory(
        uint directoryOffset,
        string rootPath,
        byte[] directoryMetadataBlock,
        byte[] fileMetadataBlock,
        ulong dataOffset,
        RichTextBox TB_Progress = null,
        ProgressBar PB_Show = null)
    {
        Romfs_DirEntry entry = GetDirEntryFromMetadataBytes(directoryMetadataBlock, Convert.ToInt32(directoryOffset));

        string currentPath = Path.Combine(rootPath, entry.Name);
        Directory.CreateDirectory(currentPath);

        if (entry.FileOffset != 0xFFFFFFFF)
        {
            VisitFile(entry.FileOffset, currentPath, fileMetadataBlock, dataOffset, TB_Progress, PB_Show);
        }

        if (entry.ChildOffset != 0xFFFFFFFF)
        {
            VisitDirectory(entry.ChildOffset, currentPath, directoryMetadataBlock, fileMetadataBlock, dataOffset, TB_Progress, PB_Show);
        }

        if (entry.SiblingOffset != 0xFFFFFFFF)
        {
            VisitDirectory(entry.SiblingOffset, rootPath, directoryMetadataBlock, fileMetadataBlock, dataOffset, TB_Progress, PB_Show);
        }
    }

    private void VisitFile(
        uint fileOffset,
        string rootPath,
        byte[] fileMetadataBlock,
        ulong dataOffset,
        RichTextBox TB_Progress = null,
        ProgressBar PB_Show = null)
    {
        Romfs_FileEntry entry = GetFileEntryFromMetadataBytes(fileMetadataBlock, Convert.ToInt32(fileOffset));
        string currentPath = Path.Combine(rootPath, entry.Name);
        UpdateTB_Progress($"Extracting {currentPath} from romfs", TB_Progress);
        ExtractFileFromRomFS(entry, currentPath, dataOffset, PB_Show);

        if (entry.SiblingOffset != 0xFFFFFFFF)
        {
            VisitFile(entry.SiblingOffset, rootPath, fileMetadataBlock, dataOffset, TB_Progress, PB_Show);
        }
    }

    private static Romfs_DirEntry GetDirEntryFromMetadataBytes(byte[] directoryMetadataBlock, int directoryOffset)
    {
        var entry = new Romfs_DirEntry
        {
            ParentOffset = BitConverter.ToUInt32(directoryMetadataBlock, directoryOffset),
            SiblingOffset = BitConverter.ToUInt32(directoryMetadataBlock, directoryOffset + 0x04),
            ChildOffset = BitConverter.ToUInt32(directoryMetadataBlock, directoryOffset + 0x08),
            FileOffset = BitConverter.ToUInt32(directoryMetadataBlock, directoryOffset + 0x0C),
        };
        uint nameLength = BitConverter.ToUInt32(directoryMetadataBlock, directoryOffset + 0x14);
        entry.Name = "";
        if (nameLength != 0xFFFFFFFF)
        {
            byte[] fileNameBytes = new byte[nameLength];
            Array.Copy(directoryMetadataBlock, directoryOffset + 0x18, fileNameBytes, 0, fileNameBytes.Length);
            entry.Name = Encoding.Unicode.GetString(fileNameBytes);
        }
        return entry;
    }

    private static Romfs_FileEntry GetFileEntryFromMetadataBytes(byte[] fileMetadataBlock, int fileOffset)
    {
        var entry = new Romfs_FileEntry
        {
            ParentDirOffset = BitConverter.ToUInt32(fileMetadataBlock, fileOffset),
            SiblingOffset = BitConverter.ToUInt32(fileMetadataBlock, fileOffset + 0x04),
            DataOffset = BitConverter.ToUInt64(fileMetadataBlock, fileOffset + 0x08),
            DataSize = BitConverter.ToUInt64(fileMetadataBlock, fileOffset + 0x10),
            NameSize = BitConverter.ToUInt32(fileMetadataBlock, fileOffset + 0x1C),
            Name = "",
        };
        if (entry.NameSize != 0xFFFFFFFF)
        {
            byte[] fileNameBytes = new byte[entry.NameSize];
            Array.Copy(fileMetadataBlock, fileOffset + 0x20, fileNameBytes, 0, fileNameBytes.Length);
            entry.Name = Encoding.Unicode.GetString(fileNameBytes);
        }
        return entry;
    }

    private void ExtractFileFromRomFS(Romfs_FileEntry entry, string path, ulong dataOffset, ProgressBar PB_Show = null)
    {
        ulong offsetInRomFS = dataOffset + entry.DataOffset;
        byte[] buffer = new byte[0x2000];

        using FileStream romfsFileStream = new(FileName, FileMode.Open, FileAccess.Read),
            outputFileStream = new(path, FileMode.Append, FileAccess.Write);
        romfsFileStream.Seek(Convert.ToInt64(offsetInRomFS), SeekOrigin.Begin);
        int remainingSize = Convert.ToInt32(entry.DataSize);
        var max = remainingSize / buffer.Length;
        if (PB_Show.InvokeRequired)
        {
            PB_Show.Invoke((MethodInvoker)delegate { PB_Show.Minimum = 0; PB_Show.Step = 1; PB_Show.Value = 0; PB_Show.Maximum = max; });
        }
        else { PB_Show.Minimum = 0; PB_Show.Step = 1; PB_Show.Value = 0; PB_Show.Maximum = max; }

        while (remainingSize > 0)
        {
            int sizeToReadWrite = buffer.Length > remainingSize ? remainingSize : buffer.Length;
            romfsFileStream.Read(buffer, 0, sizeToReadWrite);
            outputFileStream.Write(buffer, 0, sizeToReadWrite);
            remainingSize -= sizeToReadWrite;
            if (PB_Show.InvokeRequired)
            {
                PB_Show.Invoke((MethodInvoker)PB_Show.PerformStep);
            }
            else { PB_Show.PerformStep(); }
        }
    }

    public static void BuildRomFS(string infile, string outfile, RichTextBox TB_Progress = null, ProgressBar PB_Show = null)
    {
        OutFile = outfile;
        ROOT_DIR = infile;
        if (File.Exists(TempFile)) File.Delete(TempFile);

        var FNT = new FileNameTable(ROOT_DIR);
        var RomFiles = new RomfsFile[FNT.NumFiles];
        var In = new LayoutManager.Input[FNT.NumFiles];
        UpdateTB(TB_Progress, "Creating Layout...");
        for (int i = 0; i < FNT.NumFiles; i++)
        {
            In[i] = new LayoutManager.Input { FilePath = FNT.NameEntryTable[i].FullName, AlignmentSize = 0x10 };
        }
        var Out = LayoutManager.Create(In);
        for (int i = 0; i < Out.Length; i++)
        {
            RomFiles[i] = new RomfsFile
            {
                Offset = Out[i].Offset,
                PathName = Out[i].FilePath.Replace(Path.GetFullPath(ROOT_DIR), "").Replace("\\", "/"),
                FullName = Out[i].FilePath,
                Size = Out[i].Size,
            };
        }

        using var memoryStream = new MemoryStream();
        UpdateTB(TB_Progress, "Creating RomFS MetaData...");
        BuildRomFSHeader(memoryStream, RomFiles, ROOT_DIR);
        MakeRomFSData(RomFiles, memoryStream, TB_Progress, PB_Show);
    }

    internal static ulong Align(ulong input, ulong alignsize)
    {
        ulong output = input;
        if (output % alignsize != 0)
        {
            output += alignsize - (output % alignsize);
        }
        return output;
    }

    internal static void MakeRomFSData(RomfsFile[] RomFiles, MemoryStream metadata, RichTextBox TB_Progress = null, ProgressBar PB_Show = null)
    {
        UpdateTB(TB_Progress, "Computing IVFC Header Data...");
        var ivfc = new IVFCInfo { Levels = new IVFCLevel[3] };
        for (int i = 0; i < ivfc.Levels.Length; i++)
        {
            ivfc.Levels[i] = new IVFCLevel { BlockSize = 0x1000 };
        }
        ivfc.Levels[2].DataLength = RomfsFile.GetDataBlockLength(RomFiles, (ulong)metadata.Length);
        ivfc.Levels[1].DataLength = Align(ivfc.Levels[2].DataLength, ivfc.Levels[2].BlockSize) / ivfc.Levels[2].BlockSize * 0x20; //0x20 per SHA256 hash
        ivfc.Levels[0].DataLength = Align(ivfc.Levels[1].DataLength, ivfc.Levels[1].BlockSize) / ivfc.Levels[1].BlockSize * 0x20; //0x20 per SHA256 hash
        ulong MasterHashLen = Align(ivfc.Levels[0].DataLength, ivfc.Levels[0].BlockSize) / ivfc.Levels[0].BlockSize * 0x20;
        ulong lofs = 0;
        foreach (IVFCLevel t in ivfc.Levels)
        {
            t.HashOffset = lofs;
            lofs += Align(t.DataLength, t.BlockSize);
        }
        const uint IVFC_MAGIC = 0x43465649; //IVFC
        const uint RESERVED = 0x0;
        const uint HeaderLen = 0x5C;
        const uint MEDIA_UNIT_SIZE = 0x200;
        var OutFileStream = new FileStream(TempFile, FileMode.Create, FileAccess.ReadWrite);
        try
        {
            OutFileStream.Seek(0, SeekOrigin.Begin);
            OutFileStream.Write(BitConverter.GetBytes(IVFC_MAGIC), 0, 0x4);
            OutFileStream.Write(BitConverter.GetBytes(0x10000), 0, 0x4);
            OutFileStream.Write(BitConverter.GetBytes(MasterHashLen), 0, 0x4);
            foreach (IVFCLevel t in ivfc.Levels)
            {
                OutFileStream.Write(BitConverter.GetBytes(t.HashOffset), 0, 0x8);
                OutFileStream.Write(BitConverter.GetBytes(t.DataLength), 0, 0x8);
                OutFileStream.Write(BitConverter.GetBytes((int)Math.Log(t.BlockSize, 2)), 0, 0x4);
                OutFileStream.Write(BitConverter.GetBytes(RESERVED), 0, 0x4);
            }
            OutFileStream.Write(BitConverter.GetBytes(HeaderLen), 0, 0x4);
            //IVFC Header is Written.
            OutFileStream.Seek((long)Align(MasterHashLen + 0x60, ivfc.Levels[0].BlockSize), SeekOrigin.Begin);
            byte[] metadataArray = metadata.ToArray();
            OutFileStream.Write(metadataArray, 0, metadataArray.Length);
            long baseOfs = OutFileStream.Position;
            UpdateTB(TB_Progress, "Writing Level 2 Data...");
            if (PB_Show.InvokeRequired)
            {
                PB_Show.Invoke((MethodInvoker)delegate { PB_Show.Minimum = 0; PB_Show.Step = 1; PB_Show.Value = 0; PB_Show.Maximum = RomFiles.Length; });
            }
            else { PB_Show.Minimum = 0; PB_Show.Step = 1; PB_Show.Value = 0; PB_Show.Maximum = RomFiles.Length; }

            foreach (RomfsFile t in RomFiles)
            {
                OutFileStream.Seek(baseOfs + (long)t.Offset, SeekOrigin.Begin);
                using (var inStream = new FileStream(t.FullName, FileMode.Open, FileAccess.Read))
                {
                    while (inStream.Position < inStream.Length)
                    {
                        byte[] buffer = new byte[inStream.Length - inStream.Position > 0x100000 ? 0x100000 : inStream.Length - inStream.Position];
                        _ = inStream.Read(buffer, 0, buffer.Length);
                        OutFileStream.Write(buffer, 0, buffer.Length);
                    }
                }
                if (PB_Show.InvokeRequired)
                {
                    PB_Show.Invoke((MethodInvoker)PB_Show.PerformStep);
                }
                else { PB_Show.PerformStep(); }
            }
            long hashBaseOfs = (long)Align((ulong)OutFileStream.Position, ivfc.Levels[2].BlockSize);
            long hOfs = (long)Align(MasterHashLen, ivfc.Levels[0].BlockSize);
            long cOfs = hashBaseOfs + (long)ivfc.Levels[1].HashOffset;
            for (int i = ivfc.Levels.Length - 1; i >= 0; i--)
            {
                UpdateTB(TB_Progress, "Computing Level " + i + " Hashes...");
                byte[] buffer = new byte[(int)ivfc.Levels[i].BlockSize];

                var count = (int)(ivfc.Levels[i].DataLength / ivfc.Levels[i].BlockSize);
                if (PB_Show.InvokeRequired)
                {
                    PB_Show.Invoke((MethodInvoker)delegate { PB_Show.Minimum = 0; PB_Show.Step = 1; PB_Show.Value = 0; PB_Show.Maximum = count; });
                }
                else { PB_Show.Minimum = 0; PB_Show.Step = 1; PB_Show.Value = 0; PB_Show.Maximum = count; }

                for (long ofs = 0; ofs < (long)ivfc.Levels[i].DataLength; ofs += ivfc.Levels[i].BlockSize)
                {
                    OutFileStream.Seek(hOfs, SeekOrigin.Begin);
                    OutFileStream.Read(buffer, 0, (int)ivfc.Levels[i].BlockSize);
                    hOfs = OutFileStream.Position;
                    byte[] hash = SHA256.HashData(buffer);
                    OutFileStream.Seek(cOfs, SeekOrigin.Begin);
                    OutFileStream.Write(hash, 0, hash.Length);
                    cOfs = OutFileStream.Position;
                    if (PB_Show.InvokeRequired)
                    {
                        PB_Show.Invoke((MethodInvoker)PB_Show.PerformStep);
                    }
                    else { PB_Show.PerformStep(); }
                }

                if (i <= 0)
                    continue;

                if (i == 2)
                {
                    long len = OutFileStream.Position;
                    if (len % 0x1000 != 0)
                    {
                        len = (long)Align((ulong)len, 0x1000);
                        byte[] buf = new byte[len - OutFileStream.Position];
                        OutFileStream.Write(buf, 0, buf.Length);
                    }
                }

                hOfs = hashBaseOfs + (long)ivfc.Levels[i - 1].HashOffset;
                if (i > 1)
                    cOfs = hashBaseOfs + (long)ivfc.Levels[i - 2].HashOffset;
                else
                    cOfs = (long)Align(HeaderLen, PADDING_ALIGN);
            }
            OutFileStream.Seek(0, SeekOrigin.Begin);
            uint SuperBlockLen = (uint)Align(MasterHashLen + 0x60, MEDIA_UNIT_SIZE);
            byte[] MasterHashes = new byte[SuperBlockLen];
            OutFileStream.Read(MasterHashes, 0, (int)SuperBlockLen);

            //var SuperBlockHash = sha.ComputeHash(MasterHashes);
            //Console.WriteLine(SuperBlockHash);
        }
        finally
        {
            OutFileStream.Dispose();
        }

        if (OutFile == TempFile)
            return;
        if (File.Exists(OutFile)) File.Delete(OutFile);
        File.Move(TempFile, OutFile);
    }

    internal static void WriteBinary(string tempFile, string outFile, RichTextBox TB_Progress = null, ProgressBar PB_Show = null)
    {
        using var fs = new FileStream(outFile, FileMode.Create);
        using var writer = new BinaryWriter(fs);
        using var fileStream = new FileStream(tempFile, FileMode.Open, FileAccess.Read);

        const uint BUFFER_SIZE = 0x400000; // 4MB Buffer
        var steps = (int)(fileStream.Length / BUFFER_SIZE);
        if (PB_Show.InvokeRequired)
        {
            PB_Show.Invoke((MethodInvoker)delegate { PB_Show.Minimum = 0; PB_Show.Step = 1; PB_Show.Value = 0; PB_Show.Maximum = steps; });
        }
        else { PB_Show.Minimum = 0; PB_Show.Step = 1; PB_Show.Value = 0; PB_Show.Maximum = steps; }

        byte[] buffer = new byte[BUFFER_SIZE];
        while (true)
        {
            int count = fileStream.Read(buffer, 0, buffer.Length);
            if (count != 0)
            {
                writer.Write(buffer, 0, count);
                if (PB_Show.InvokeRequired)
                {
                    PB_Show.Invoke((MethodInvoker)PB_Show.PerformStep);
                }
                else { PB_Show.PerformStep(); }
            }
            else
            {
                break;
            }
        }

        writer.Flush();
        File.Delete(TempFile);
        UpdateTB(TB_Progress, "Wrote RomFS to path:" + Environment.NewLine + outFile);
    }

    internal static string ByteArrayToString(IEnumerable<byte> input)
    {
        var sb = new StringBuilder();
        foreach (byte b in input)
            sb.Append(b.ToString("X2")).Append(' ');

        return sb.ToString();
    }

    internal static void UpdateTB_Progress(string text, RichTextBox TB_Progress = null)
    {
        if (TB_Progress.InvokeRequired)
        {
            TB_Progress.Invoke((MethodInvoker)delegate
            {
                TB_Progress.Text += text + Environment.NewLine;
            });
        }
        else
        {
            TB_Progress.Text += text + Environment.NewLine;
        }
    }

    internal static void BuildRomFSHeader(MemoryStream romfs_stream, RomfsFile[] Entries, string DIR)
    {
        ROOT_DIR = DIR;
        var MetaData = new Romfs_MetaData();

        InitializeMetaData(MetaData);
        CalcRomfsSize(MetaData);
        PopulateRomfs(MetaData, Entries);
        WriteMetaDataToStream(MetaData, romfs_stream);
    }

    internal static void InitializeMetaData(Romfs_MetaData MetaData)
    {
        MetaData.InfoHeader = new();
        MetaData.DirTable = new();
        MetaData.DirTableLen = 0;
        MetaData.M_DirTableLen = 0;
        MetaData.FileTable = new();
        MetaData.FileTableLen = 0;
        MetaData.DirTable.DirectoryTable = [];
        MetaData.FileTable.FileTable = [];
        MetaData.InfoHeader.HeaderLength = 0x28;
        MetaData.InfoHeader.Sections = new Romfs_SectionHeader[4];
        MetaData.DirHashTable = [];
        MetaData.FileHashTable = [];
    }

    internal static void CalcRomfsSize(Romfs_MetaData MetaData)
    {
        MetaData.DirNum = 1;
        var Root_DI = new DirectoryInfo(ROOT_DIR);
        CalcDirSize(MetaData, Root_DI);

        MetaData.M_DirHashTableEntry = GetHashTableEntryCount(MetaData.DirNum);
        MetaData.M_FileHashTableEntry = GetHashTableEntryCount(MetaData.FileNum);

        uint MetaDataSize = (uint)Align(0x28 + (MetaData.M_DirHashTableEntry * 4) + MetaData.M_DirTableLen + (MetaData.M_FileHashTableEntry * 4) + MetaData.M_FileTableLen, PADDING_ALIGN);
        for (int i = 0; i < MetaData.M_DirHashTableEntry; i++)
            MetaData.DirHashTable.Add(ROMFS_UNUSED_ENTRY);

        for (int i = 0; i < MetaData.M_FileHashTableEntry; i++)
            MetaData.FileHashTable.Add(ROMFS_UNUSED_ENTRY);

        uint Pos = MetaData.InfoHeader.HeaderLength;
        for (int i = 0; i < 4; i++)
        {
            MetaData.InfoHeader.Sections[i].Offset = Pos;
            uint size = i switch
            {
                0 => MetaData.M_DirHashTableEntry * 4,
                1 => MetaData.M_DirTableLen,
                2 => MetaData.M_FileHashTableEntry * 4,
                3 => MetaData.M_FileTableLen,
                _ => 0
            };
            MetaData.InfoHeader.Sections[i].Size = size;
            Pos += size;
        }
        MetaData.InfoHeader.DataOffset = MetaDataSize;
    }

    internal static uint GetHashTableEntryCount(uint Entries)
    {
        uint count = Entries;
        if (Entries < 3)
        {
            count = 3;
        }
        else if (count < 19)
        {
            count |= 1;
        }
        else
        {
            while (count % 2 == 0 || count % 3 == 0 || count % 5 == 0 || count % 7 == 0 || count % 11 == 0 || count % 13 == 0 || count % 17 == 0)
            {
                count++;
            }
        }
        return count;
    }

    internal static void CalcDirSize(Romfs_MetaData MetaData, DirectoryInfo dir)
    {
        if (MetaData.M_DirTableLen == 0)
            MetaData.M_DirTableLen = 0x18;
        else
            MetaData.M_DirTableLen += 0x18 + (uint)Align((ulong)dir.Name.Length * 2, 4);

        FileInfo[] files = dir.GetFiles();
        foreach (FileInfo t in files)
            MetaData.M_FileTableLen += 0x20 + (uint)Align((ulong)t.Name.Length * 2, 4);

        DirectoryInfo[] SubDirectories = dir.GetDirectories();
        foreach (DirectoryInfo t in SubDirectories)
            CalcDirSize(MetaData, t);

        MetaData.FileNum += (uint)files.Length;
        MetaData.DirNum += (uint)SubDirectories.Length;
    }

    internal static void PopulateRomfs(Romfs_MetaData MetaData, RomfsFile[] Entries)
    {
        //Recursively Add All Directories to DirectoryTable
        AddDir(MetaData, new DirectoryInfo(ROOT_DIR), 0, ROMFS_UNUSED_ENTRY);

        //Iteratively Add All Files to FileTable
        AddFiles(MetaData, Entries);

        //Set Weird Offsets, Build HashKeyPointers, Build HashTables
        PopulateHashTables(MetaData);

        //Thats it.
    }

    internal static void PopulateHashTables(Romfs_MetaData MetaData)
    {
        for (int i = 0; i < MetaData.DirTable.DirectoryTable.Count; i++)
            AddDirHashKey(MetaData, i);
        for (int i = 0; i < MetaData.FileTable.FileTable.Count; i++)
            AddFileHashKey(MetaData, i);
    }

    internal static void AddDirHashKey(Romfs_MetaData MetaData, int index)
    {
        uint parent = MetaData.DirTable.DirectoryTable[index].ParentOffset;
        string Name = MetaData.DirTable.DirectoryTable[index].Name;
        byte[] NArr = index == 0 ? Encoding.Unicode.GetBytes("") : Encoding.Unicode.GetBytes(Name);
        uint hash = CalcPathHash(parent, NArr, 0);
        int ind2 = (int)(hash % MetaData.M_DirHashTableEntry);
        if (MetaData.DirHashTable[ind2] == ROMFS_UNUSED_ENTRY)
        {
            MetaData.DirHashTable[ind2] = MetaData.DirTable.DirectoryTable[index].Offset;
        }
        else
        {
            int i = GetRomfsDirEntry(MetaData, MetaData.DirHashTable[ind2]);
            int tempindex = index;
            MetaData.DirHashTable[ind2] = MetaData.DirTable.DirectoryTable[index].Offset;
            while (true)
            {
                if (MetaData.DirTable.DirectoryTable[tempindex].HashKeyPointer == ROMFS_UNUSED_ENTRY)
                {
                    MetaData.DirTable.DirectoryTable[tempindex].HashKeyPointer = MetaData.DirTable.DirectoryTable[i].Offset;
                    break;
                }
                i = tempindex;
                tempindex = GetRomfsDirEntry(MetaData, MetaData.DirTable.DirectoryTable[i].HashKeyPointer);
            }
        }
    }

    internal static void AddFileHashKey(Romfs_MetaData MetaData, int index)
    {
        uint parent = MetaData.FileTable.FileTable[index].ParentDirOffset;
        string Name = MetaData.FileTable.FileTable[index].Name;
        byte[] NArr = Encoding.Unicode.GetBytes(Name);
        uint hash = CalcPathHash(parent, NArr, 0);
        int ind2 = (int)(hash % MetaData.M_FileHashTableEntry);
        if (MetaData.FileHashTable[ind2] == ROMFS_UNUSED_ENTRY)
        {
            MetaData.FileHashTable[ind2] = MetaData.FileTable.FileTable[index].Offset;
        }
        else
        {
            int i = GetRomfsFileEntry(MetaData, MetaData.FileHashTable[ind2]);
            int tempindex = index;
            MetaData.FileHashTable[ind2] = MetaData.FileTable.FileTable[index].Offset;
            while (true)
            {
                if (MetaData.FileTable.FileTable[tempindex].HashKeyPointer == ROMFS_UNUSED_ENTRY)
                {
                    MetaData.FileTable.FileTable[tempindex].HashKeyPointer = MetaData.FileTable.FileTable[i].Offset;
                    break;
                }
                i = tempindex;
                tempindex = GetRomfsFileEntry(MetaData, MetaData.FileTable.FileTable[i].HashKeyPointer);
            }
        }
    }

    internal static uint CalcPathHash(uint ParentOffset, byte[] NameArray, int start)
    {
        uint hash = ParentOffset ^ 123456789;
        for (int i = 0; i < NameArray.Length; i += 2)
        {
            hash = (hash >> 5) | (hash << 27);
            hash ^= (ushort)(NameArray[start + i] | (NameArray[start + i + 1] << 8));
        }
        return hash;
    }

    internal static void AddDir(Romfs_MetaData MetaData, DirectoryInfo Dir, uint parent, uint sibling)
    {
        AddDir(MetaData, Dir, parent, sibling, false);
        AddDir(MetaData, Dir, parent, sibling, true);
    }

    internal static void AddDir(Romfs_MetaData MetaData, DirectoryInfo Dir, uint parent, uint sibling, bool DoSubs)
    {
        DirectoryInfo[] SubDirectories = Dir.GetDirectories();
        if (!DoSubs)
        {
            uint CurrentDir = MetaData.DirTableLen;
            var Entry = new Romfs_DirEntry { ParentOffset = parent };
            Entry.ChildOffset = Entry.HashKeyPointer = Entry.FileOffset = ROMFS_UNUSED_ENTRY;
            Entry.SiblingOffset = sibling;
            Entry.FullName = Dir.FullName;
            Entry.Name = Entry.FullName == ROOT_DIR ? "" : Dir.Name;
            Entry.Offset = CurrentDir;
            MetaData.DirTable.DirectoryTable.Add(Entry);
            MetaData.DirTableLen += CurrentDir == 0 ? 0x18 : 0x18 + (uint)Align((ulong)Dir.Name.Length * 2, 4);
            // int ParentIndex = GetRomfsDirEntry(MetaData, Dir.FullName);
            // uint poff = MetaData.DirTable.DirectoryTable[ParentIndex].Offset;
        }
        else
        {
            int CurIndex = GetRomfsDirEntry(MetaData, Dir.FullName);
            uint CurrentDir = MetaData.DirTable.DirectoryTable[CurIndex].Offset;
            for (int i = 0; i < SubDirectories.Length; i++)
            {
                AddDir(MetaData, SubDirectories[i], CurrentDir, sibling, false);
                if (i <= 0)
                    continue;

                string PrevFullName = SubDirectories[i - 1].FullName;
                string ThisName = SubDirectories[i].FullName;
                int PrevIndex = GetRomfsDirEntry(MetaData, PrevFullName);
                int ThisIndex = GetRomfsDirEntry(MetaData, ThisName);
                MetaData.DirTable.DirectoryTable[PrevIndex].SiblingOffset =
                    MetaData.DirTable.DirectoryTable[ThisIndex].Offset;
            }
            foreach (DirectoryInfo t in SubDirectories)
                AddDir(MetaData, t, CurrentDir, sibling, true);
        }

        if (SubDirectories.Length == 0)
            return;

        int curindex = GetRomfsDirEntry(MetaData, Dir.FullName);
        int childindex = GetRomfsDirEntry(MetaData, SubDirectories[0].FullName);
        if (curindex > -1 && childindex > -1)
            MetaData.DirTable.DirectoryTable[curindex].ChildOffset = MetaData.DirTable.DirectoryTable[childindex].Offset;
    }

    internal static void AddFiles(Romfs_MetaData MetaData, RomfsFile[] Entries)
    {
        string PrevDirPath = "";
        for (int i = 0; i < Entries.Length; i++)
        {
            var file = new FileInfo(Entries[i].FullName);
            var Entry = new Romfs_FileEntry();
            string DirPath = Path.GetDirectoryName(Entries[i].FullName);
            int ParentIndex = GetRomfsDirEntry(MetaData, DirPath);
            Entry.FullName = Entries[i].FullName;
            Entry.Offset = MetaData.FileTableLen;
            Entry.ParentDirOffset = MetaData.DirTable.DirectoryTable[ParentIndex].Offset;
            Entry.SiblingOffset = ROMFS_UNUSED_ENTRY;
            if (DirPath == PrevDirPath)
            {
                MetaData.FileTable.FileTable[i - 1].SiblingOffset = Entry.Offset;
            }
            if (MetaData.DirTable.DirectoryTable[ParentIndex].FileOffset == ROMFS_UNUSED_ENTRY)
            {
                MetaData.DirTable.DirectoryTable[ParentIndex].FileOffset = Entry.Offset;
            }
            Entry.HashKeyPointer = ROMFS_UNUSED_ENTRY;
            Entry.NameSize = (uint)file.Name.Length * 2;
            Entry.Name = file.Name;
            Entry.DataOffset = Entries[i].Offset;
            Entry.DataSize = Entries[i].Size;
            MetaData.FileTable.FileTable.Add(Entry);
            MetaData.FileTableLen += 0x20 + (uint)Align((ulong)file.Name.Length * 2, 4);
            PrevDirPath = DirPath;
        }
    }

    internal static void WriteMetaDataToStream(Romfs_MetaData MetaData, MemoryStream stream)
    {
        //First, InfoHeader.
        stream.Write(BitConverter.GetBytes(MetaData.InfoHeader.HeaderLength), 0, 4);
        foreach (Romfs_SectionHeader SH in MetaData.InfoHeader.Sections)
        {
            stream.Write(BitConverter.GetBytes(SH.Offset), 0, 4);
            stream.Write(BitConverter.GetBytes(SH.Size), 0, 4);
        }
        stream.Write(BitConverter.GetBytes(MetaData.InfoHeader.DataOffset), 0, 4);

        //DirHashTable
        foreach (uint u in MetaData.DirHashTable)
        {
            stream.Write(BitConverter.GetBytes(u), 0, 4);
        }

        //DirTable
        foreach (Romfs_DirEntry dir in MetaData.DirTable.DirectoryTable)
        {
            stream.Write(BitConverter.GetBytes(dir.ParentOffset), 0, 4);
            stream.Write(BitConverter.GetBytes(dir.SiblingOffset), 0, 4);
            stream.Write(BitConverter.GetBytes(dir.ChildOffset), 0, 4);
            stream.Write(BitConverter.GetBytes(dir.FileOffset), 0, 4);
            stream.Write(BitConverter.GetBytes(dir.HashKeyPointer), 0, 4);
            uint nlen = (uint)dir.Name.Length * 2;
            stream.Write(BitConverter.GetBytes(nlen), 0, 4);
            byte[] NameArray = new byte[(int)Align(nlen, 4)];
            Array.Copy(Encoding.Unicode.GetBytes(dir.Name), 0, NameArray, 0, nlen);
            stream.Write(NameArray, 0, NameArray.Length);
        }

        //FileHashTable
        foreach (uint u in MetaData.FileHashTable)
        {
            stream.Write(BitConverter.GetBytes(u), 0, 4);
        }

        //FileTable
        foreach (Romfs_FileEntry file in MetaData.FileTable.FileTable)
        {
            stream.Write(BitConverter.GetBytes(file.ParentDirOffset), 0, 4);
            stream.Write(BitConverter.GetBytes(file.SiblingOffset), 0, 4);
            stream.Write(BitConverter.GetBytes(file.DataOffset), 0, 8);
            stream.Write(BitConverter.GetBytes(file.DataSize), 0, 8);
            stream.Write(BitConverter.GetBytes(file.HashKeyPointer), 0, 4);
            uint nlen = (uint)file.Name.Length * 2;
            stream.Write(BitConverter.GetBytes(nlen), 0, 4);
            byte[] NameArray = new byte[(int)Align(nlen, 4)];
            Array.Copy(Encoding.Unicode.GetBytes(file.Name), 0, NameArray, 0, nlen);
            stream.Write(NameArray, 0, NameArray.Length);
        }

        //Padding
        while (stream.Position % PADDING_ALIGN != 0)
            stream.Write(new byte[PADDING_ALIGN - (stream.Position % 0x10)], 0, (int)(PADDING_ALIGN - (stream.Position % 0x10)));
        //All Done.
    }

    //GetRomfs[...]Entry Functions are all O(n)
    internal static int GetRomfsDirEntry(Romfs_MetaData MetaData, string FullName)
    {
        for (int i = 0; i < MetaData.DirTable.DirectoryTable.Count; i++)
        {
            if (MetaData.DirTable.DirectoryTable[i].FullName == FullName)
                return i;
        }

        return -1;
    }

    internal static int GetRomfsDirEntry(Romfs_MetaData MetaData, uint Offset)
    {
        for (int i = 0; i < MetaData.DirTable.DirectoryTable.Count; i++)
        {
            if (MetaData.DirTable.DirectoryTable[i].Offset == Offset)
                return i;
        }

        return -1;
    }

    internal static int GetRomfsFileEntry(Romfs_MetaData MetaData, uint Offset)
    {
        for (int i = 0; i < MetaData.FileTable.FileTable.Count; i++)
        {
            if (MetaData.FileTable.FileTable[i].Offset == Offset)
                return i;
        }

        return -1;
    }

    #region Support Class/Struct
    public class Romfs_MetaData
    {
        public Romfs_InfoHeader InfoHeader;
        public uint DirNum;
        public uint FileNum;
        public List<uint> DirHashTable;
        public uint M_DirHashTableEntry;
        public Romfs_DirTable DirTable;
        public uint DirTableLen;
        public uint M_DirTableLen;
        public List<uint> FileHashTable;
        public uint M_FileHashTableEntry;
        public Romfs_FileTable FileTable;
        public uint FileTableLen;
        public uint M_FileTableLen;
    }

    public struct Romfs_SectionHeader
    {
        public uint Offset;
        public uint Size;
    }

    public struct Romfs_InfoHeader
    {
        public uint HeaderLength;
        public Romfs_SectionHeader[] Sections;
        public uint DataOffset;
    }

    public class Romfs_DirTable
    {
        public List<Romfs_DirEntry> DirectoryTable;
    }

    public class Romfs_FileTable
    {
        public List<Romfs_FileEntry> FileTable;
    }

    public class Romfs_DirEntry
    {
        public uint ParentOffset;
        public uint SiblingOffset;
        public uint ChildOffset;
        public uint FileOffset;
        public uint HashKeyPointer;
        public string Name;
        public string FullName;
        public uint Offset;
    }

    public class Romfs_FileEntry
    {
        public uint ParentDirOffset;
        public uint SiblingOffset;
        public ulong DataOffset;
        public ulong DataSize;
        public uint HashKeyPointer;
        public uint NameSize;
        public string Name;
        public string FullName;
        public uint Offset;
    }

    public class RomfsFile
    {
        public string PathName;
        public ulong Offset;
        public ulong Size;
        public string FullName;

        public static ulong GetDataBlockLength(RomfsFile[] files, ulong PreData)
        {
            return files.Length == 0 ? PreData : PreData + files[^1].Offset + files[^1].Size;
        }
    }

    public class IVFCInfo
    {
        public uint MasterHashSize;
        public IVFCLevel[] Levels;
    }

    public class IVFCLevel
    {
        public ulong HashOffset;
        public ulong DataLength;
        public uint BlockSize;
    }

    public class FileNameTable
    {
        public List<FileInfo> NameEntryTable { get; }
        public int NumFiles => NameEntryTable.Count;

        internal FileNameTable(string rootPath)
        {
            NameEntryTable = [];
            AddDirectory(new DirectoryInfo(rootPath));
        }

        internal void AddDirectory(DirectoryInfo dir)
        {
            NameEntryTable.AddRange(dir.GetFiles());
            foreach (DirectoryInfo subdir in dir.GetDirectories())
            {
                AddDirectory(subdir);
            }
        }
    }

    public static class LayoutManager
    {
        public static Output[] Create(IEnumerable<Input> Input)
        {
            List<Output> list = [];
            ulong Len = 0;
            foreach (Input input in Input)
            {
                var output = new Output();
                var fileInfo = new FileInfo(input.FilePath);
                ulong ofs = AlignInput(Len, input.AlignmentSize);
                output.FilePath = input.FilePath;
                output.Offset = ofs;
                output.Size = (ulong)fileInfo.Length;
                list.Add(output);
                Len = ofs + (ulong)fileInfo.Length;
            }
            return [.. list];
        }

        private static ulong AlignInput(ulong input, ulong alignsize)
        {
            ulong output = input;
            if (output % alignsize != 0)
                output += alignsize - (output % alignsize);

            return output;
        }

        public class Input
        {
            public string FilePath;
            public uint AlignmentSize;
        }

        public class Output
        {
            public string FilePath;
            public ulong Offset;
            public ulong Size;
        }
    }
    #endregion
}